#!/bin/bash
###############################################################################
#       脚本名称: appender_file.sh
#       功能描述: 文件输出器,将日志写入文件,支持轮转和归档
#       作者: 林再来
#       日期: 2025-10-14
#       版本: 1.0
#       功能:
#           1、支持日志文件写入
#           2、支持按大小轮转
#           3、支持按时间轮转
#           4、自动创建目录
#           5、文件锁保证并发安全
#       依赖(插件/命令):
#           flock(文件锁)
#           gzip(压缩,可选)
###############################################################################

# 防止重复加载
[[ -n "${_APPENDER_FILE_LOADED:-}" ]] && return 0
readonly _APPENDER_FILE_LOADED=1

#-----------------------------------------------------------------------------
# 全局变量
#-----------------------------------------------------------------------------
# 存储每个appender的配置
declare -gA _FILE_APPENDER_CONFIGS
# 存储文件句柄
declare -gA _FILE_APPENDER_FDS

#-----------------------------------------------------------------------------
# 默认配置（参考config模块的做法，先检查是否已定义）
#-----------------------------------------------------------------------------
if [[ -z "${DEFAULT_FILE_PATH:-}" ]]; then
    readonly DEFAULT_FILE_PATH="logs/app/app.log"
fi
if [[ -z "${DEFAULT_MAX_SIZE:-}" ]]; then
    readonly DEFAULT_MAX_SIZE="100M"      # 100MB
fi
if [[ -z "${DEFAULT_MAX_FILES:-}" ]]; then
    readonly DEFAULT_MAX_FILES=10
fi
if [[ -z "${DEFAULT_FILE_MODE:-}" ]]; then
    readonly DEFAULT_FILE_MODE="0644"
fi

#-----------------------------------------------------------------------------
# 函数: 解析大小字符串为字节数
# 参数: $1 - 大小字符串(如"100M", "1G", "500K")
# 返回: 输出字节数
#-----------------------------------------------------------------------------
function _parse_size() {
    local size_str="$1"
    
    # 提取数字部分
    local number="${size_str//[^0-9]/}"
    # 提取单位部分
    local unit="${size_str//[0-9]/}"
    unit="${unit^^}"  # 转大写
    
    case "$unit" in
        K|KB)
            echo $((number * 1024))
            ;;
        M|MB)
            echo $((number * 1024 * 1024))
            ;;
        G|GB)
            echo $((number * 1024 * 1024 * 1024))
            ;;
        *)
            echo "$number"  # 默认为字节
            ;;
    esac
}

#-----------------------------------------------------------------------------
# 函数: 获取文件大小(字节)
# 参数: $1 - 文件路径
# 返回: 输出文件大小,文件不存在返回0
#-----------------------------------------------------------------------------
function _get_file_size() {
    local file="$1"
    
    [[ ! -f "$file" ]] && echo "0" && return 0
    
    # 使用stat命令获取文件大小
    if command -v stat &>/dev/null; then
        # Linux
        stat -c %s "$file" 2>/dev/null || echo "0"
    else
        # 备用方法
        wc -c < "$file" 2>/dev/null || echo "0"
    fi
}

#-----------------------------------------------------------------------------
# 函数: 初始化文件输出器
# 参数: $1 - appender名称
# 返回: 0-成功, 1-失败
#-----------------------------------------------------------------------------
function _appender_file_init() {
    local appender_name="$1"
    
    # 初始化配置(TODO: 从配置文件读取)
    _FILE_APPENDER_CONFIGS["${appender_name}_file"]="$DEFAULT_FILE_PATH"
    _FILE_APPENDER_CONFIGS["${appender_name}_max_size"]="$DEFAULT_MAX_SIZE"
    _FILE_APPENDER_CONFIGS["${appender_name}_max_files"]="$DEFAULT_MAX_FILES"
    _FILE_APPENDER_CONFIGS["${appender_name}_rotation_strategy"]="size"  # size, daily, hourly
    _FILE_APPENDER_CONFIGS["${appender_name}_compress"]="false"
    _FILE_APPENDER_CONFIGS["${appender_name}_file_mode"]="$DEFAULT_FILE_MODE"
    _FILE_APPENDER_CONFIGS["${appender_name}_last_rotation_date"]=""
    _FILE_APPENDER_CONFIGS["${appender_name}_initialized"]="false"
    
    # 注意：文件创建延迟到第一次写入时，这样可以确保使用正确的配置路径
    # 不在这里创建文件了
    
    return 0
}

#-----------------------------------------------------------------------------
# 函数: 确保日志文件存在
# 参数: $1 - appender名称
# 返回: 0-成功, 1-失败
#-----------------------------------------------------------------------------
function _ensure_log_file() {
    local appender_name="$1"
    local log_file="${_FILE_APPENDER_CONFIGS[${appender_name}_file]}"
    local log_dir=$(dirname "$log_file")
    
    # 如果已经初始化过，直接返回
    [[ "${_FILE_APPENDER_CONFIGS[${appender_name}_initialized]}" == "true" ]] && return 0
    
    # 创建日志目录
    if [[ ! -d "$log_dir" ]]; then
        mkdir -p "$log_dir" || {
            echo "错误: 无法创建日志目录: $log_dir" >&2
            return 1
        }
    fi
    
    # 创建文件(如果不存在)
    if [[ ! -f "$log_file" ]]; then
        touch "$log_file" || {
            echo "错误: 无法创建日志文件: $log_file" >&2
            return 1
        }
        
        # 设置文件权限
        local file_mode="${_FILE_APPENDER_CONFIGS[${appender_name}_file_mode]}"
        chmod "$file_mode" "$log_file" 2>/dev/null
    fi
    
    # 标记为已初始化
    _FILE_APPENDER_CONFIGS["${appender_name}_initialized"]="true"
    
    return 0
}

#-----------------------------------------------------------------------------
# 函数: 检查是否需要轮转
# 参数: $1 - appender名称
# 返回: 0-需要轮转, 1-不需要轮转
#-----------------------------------------------------------------------------
function _should_rotate() {
    local appender_name="$1"
    local log_file="${_FILE_APPENDER_CONFIGS[${appender_name}_file]}"
    local strategy="${_FILE_APPENDER_CONFIGS[${appender_name}_rotation_strategy]}"
    
    case "$strategy" in
        size)
            # 按大小轮转
            local max_size_str="${_FILE_APPENDER_CONFIGS[${appender_name}_max_size]}"
            local max_size=$(_parse_size "$max_size_str")
            local current_size=$(_get_file_size "$log_file")
            
            [[ $current_size -ge $max_size ]]
            ;;
        daily)
            # 按天轮转
            local current_date=$(date +%Y%m%d)
            local last_date="${_FILE_APPENDER_CONFIGS[${appender_name}_last_rotation_date]}"
            
            [[ "$current_date" != "$last_date" ]]
            ;;
        hourly)
            # 按小时轮转
            local current_hour=$(date +%Y%m%d%H)
            local last_hour="${_FILE_APPENDER_CONFIGS[${appender_name}_last_rotation_date]}"
            
            [[ "$current_hour" != "$last_hour" ]]
            ;;
        *)
            return 1  # 未知策略,不轮转
            ;;
    esac
}

#-----------------------------------------------------------------------------
# 函数: 执行日志轮转
# 参数: $1 - appender名称
# 返回: 0-成功, 1-失败
#-----------------------------------------------------------------------------
function _rotate_log_file() {
    local appender_name="$1"
    local log_file="${_FILE_APPENDER_CONFIGS[${appender_name}_file]}"
    local max_files="${_FILE_APPENDER_CONFIGS[${appender_name}_max_files]}"
    local compress="${_FILE_APPENDER_CONFIGS[${appender_name}_compress]}"
    local strategy="${_FILE_APPENDER_CONFIGS[${appender_name}_rotation_strategy]}"
    
    # 文件不存在或为空,无需轮转
    [[ ! -f "$log_file" ]] && return 0
    [[ ! -s "$log_file" ]] && return 0
    
    # 生成轮转文件名
    local rotated_name
    if [[ "$strategy" == "size" ]]; then
        # 按序号命名: app.log.1, app.log.2, ...
        # 先移动旧文件
        local i=$((max_files - 1))
        while [[ $i -gt 0 ]]; do
            if [[ -f "${log_file}.$i" ]]; then
                if [[ $i -eq $((max_files - 1)) ]]; then
                    # 删除最旧的文件
                    rm -f "${log_file}.$i" "${log_file}.$i.gz"
                else
                    # 移动到下一个序号
                    mv "${log_file}.$i" "${log_file}.$((i + 1))"
                fi
            fi
            i=$((i - 1))
        done
        
        rotated_name="${log_file}.1"
    else
        # 按日期/时间命名: app.log.20251014
        local timestamp
        if [[ "$strategy" == "daily" ]]; then
            timestamp=$(date +%Y%m%d)
        elif [[ "$strategy" == "hourly" ]]; then
            timestamp=$(date +%Y%m%d-%H)
        else
            timestamp=$(date +%Y%m%d-%H%M%S)
        fi
        
        rotated_name="${log_file}.${timestamp}"
    fi
    
    # 执行轮转(使用移动而非复制)
    mv "$log_file" "$rotated_name" || {
        echo "错误: 日志轮转失败: $log_file -> $rotated_name" >&2
        return 1
    }
    
    # 创建新的日志文件
    touch "$log_file"
    local file_mode="${_FILE_APPENDER_CONFIGS[${appender_name}_file_mode]}"
    chmod "$file_mode" "$log_file" 2>/dev/null
    
    # 压缩轮转文件(异步执行)
    if [[ "$compress" == "true" ]] && command -v gzip &>/dev/null; then
        nohup gzip -f "$rotated_name" >/dev/null 2>&1 &
    fi
    
    # 更新最后轮转时间
    if [[ "$strategy" == "daily" ]]; then
        _FILE_APPENDER_CONFIGS["${appender_name}_last_rotation_date"]=$(date +%Y%m%d)
    elif [[ "$strategy" == "hourly" ]]; then
        _FILE_APPENDER_CONFIGS["${appender_name}_last_rotation_date"]=$(date +%Y%m%d%H)
    fi
    
    return 0
}

#-----------------------------------------------------------------------------
# 函数: 写入日志到文件
# 参数: $1 - appender名称
#       $2 - 格式化后的日志文本
#       $3 - 日志事件JSON(可选)
# 返回: 0-成功, 1-失败
#-----------------------------------------------------------------------------
function _appender_file_write() {
    local appender_name="$1"
    local formatted_log="$2"
    local event="${3:-}"
    
    # 确保日志文件存在（延迟初始化）
    _ensure_log_file "$appender_name" || return 1
    
    local log_file="${_FILE_APPENDER_CONFIGS[${appender_name}_file]}"
    
    # 检查是否需要轮转
    if _should_rotate "$appender_name"; then
        _rotate_log_file "$appender_name"
    fi
    
    # 使用文件锁写入(保证并发安全)
    {
        flock -x 200
        echo "$formatted_log" >> "$log_file"
    } 200>"${log_file}.lock"
    
    return 0
}

#-----------------------------------------------------------------------------
# 函数: 刷新缓冲区
# 参数: $1 - appender名称
# 返回: 0-成功
#-----------------------------------------------------------------------------
function _appender_file_flush() {
    local appender_name="$1"
    local log_file="${_FILE_APPENDER_CONFIGS[${appender_name}_file]}"
    
    # 强制刷新文件系统缓冲
    [[ -f "$log_file" ]] && sync "$log_file" 2>/dev/null
    
    return 0
}

#-----------------------------------------------------------------------------
# 函数: 关闭文件输出器
# 参数: $1 - appender名称
# 返回: 0-成功
#-----------------------------------------------------------------------------
function _appender_file_close() {
    local appender_name="$1"
    
    # 刷新缓冲区
    _appender_file_flush "$appender_name"
    
    # 清理配置
    for key in "${!_FILE_APPENDER_CONFIGS[@]}"; do
        if [[ "$key" == "${appender_name}_"* ]]; then
            unset _FILE_APPENDER_CONFIGS["$key"]
        fi
    done
    
    # 删除锁文件
    local log_file="${_FILE_APPENDER_CONFIGS[${appender_name}_file]:-}"
    [[ -n "$log_file" ]] && rm -f "${log_file}.lock"
    
    return 0
}

#-----------------------------------------------------------------------------
# 函数: 设置文件输出器配置
# 参数: $1 - appender名称
#       $2 - 配置项
#       $3 - 配置值
# 返回: 0-成功, 1-失败
#-----------------------------------------------------------------------------
function _appender_file_set_config() {
    local appender_name="$1"
    local config_key="$2"
    local config_value="$3"
    
    _FILE_APPENDER_CONFIGS["${appender_name}_${config_key}"]="$config_value"
    return 0
}

#-----------------------------------------------------------------------------
# 测试函数
#-----------------------------------------------------------------------------
function _appender_file_test() {
    echo "=== 文件输出器测试 ==="
    
    # 初始化
    local test_file="temp/test_appender_file.log"
    _appender_file_init "test_file"
    _appender_file_set_config "test_file" "file" "$test_file"
    _appender_file_set_config "test_file" "max_size" "1K"  # 设置为1KB便于测试
    _appender_file_set_config "test_file" "max_files" "3"
    
    echo ""
    echo "1. 基本写入测试:"
    _appender_file_write "test_file" "[INFO] 2025-10-14 15:30:45 - 测试日志1"
    _appender_file_write "test_file" "[INFO] 2025-10-14 15:30:46 - 测试日志2"
    _appender_file_write "test_file" "[INFO] 2025-10-14 15:30:47 - 测试日志3"
    
    if [[ -f "$test_file" ]]; then
        echo "  日志文件已创建: $test_file"
        echo "  文件内容:"
        cat "$test_file" | sed 's/^/    /'
    fi
    
    echo ""
    echo "2. 文件大小检测:"
    local file_size=$(_get_file_size "$test_file")
    echo "  当前文件大小: $file_size 字节"
    
    echo ""
    echo "3. 大小解析测试:"
    echo "  100M = $(_parse_size '100M') 字节"
    echo "  1G = $(_parse_size '1G') 字节"
    echo "  500K = $(_parse_size '500K') 字节"
    
    echo ""
    echo "4. 日志轮转测试(写入大量日志触发轮转):"
    for i in {1..50}; do
        _appender_file_write "test_file" "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - 测试日志${i} - 填充数据填充数据填充数据"
    done
    
    echo "  检查轮转文件:"
    ls -lh temp/test_appender_file.log* 2>/dev/null | sed 's/^/    /' || echo "    (无轮转文件)"
    
    # 清理
    _appender_file_close "test_file"
    rm -f temp/test_appender_file.log*
}

# 如果直接执行此脚本,运行测试
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    _appender_file_test
fi

